# -*- coding: utf-8 -*-
"""
Project: P2PServer[client]
File: Client.py
Author:  laoknas
Created date: 2025/02/08
Modified date: 2025/02/08
Comment: Main server UI for P2P network.
"""

import threading
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
from cryptography.fernet import Fernet
import random
import string
import socket
import uuid
import hashlib
import os
from datetime import datetime
import logging
import base64
from pathlib import Path
from icon import img
import json
# P2P 客户端端类，包含 UI 和 客户端功能   
class ClientUI:
    """客户端图形界面"""
    def generate_random_id(self, length=8):
        """生成基于 MAC 地址的唯一客户端 ID"""
        try:
            # 获取系统的 MAC 地址
            mac = uuid.getnode()
            mac_str = ':'.join(['{:02x}'.format((mac >> elements) & 0xff) for elements in range(0, 8 * 6, 8)][::-1])
            
            # 将 MAC 地址转换为哈希值
            hash_object = hashlib.md5(mac_str.encode())
            hash_hex = hash_object.hexdigest()
            
            # 取哈希值的前 8 位作为唯一 ID
            # unique_id = hash_hex[:length]
            unique_id = hash_hex[:4] + hash_hex[-4:]    # 修改：只使用前 4 位和最后 4 位
            return unique_id
        except Exception as e:
            # 如果获取 MAC 地址失败，则生成随机 ID
            print(f"无法获取 MAC 地址，生成随机 ID: {e}")
            return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
    
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("P2P 客户端")
        self.client_id = self.generate_random_id()  # 生成随机客户端ID
        self.client_name = f"客户端-{self.client_id}"  # 默认客户端名称
        # 设置icon
        try:
            # 使用base64编码，把导入DBSP.ICO文件进行编码，装入程序中。第一次编译是取消注释，当然也可以每次编译，都重新生产icon.py文件。
            # # 此处代码为把"favicon.ico"生成icon.py文件
            # open_icon = open("favicon.ico","rb")
            # b64str = base64.b64encode(open_icon.read())
            # open_icon.close()
            # write_data = "img = %s" % b64str
            # f = open("icon.py","w+")
            # f.write(write_data)
            # f.close()

            # # 此处代码为把"DBSP.ico"生成icon.py文件
            # from icon import img
            tmp = open("tmp.ico","wb+")
            tmp.write(base64.b64decode(img))
            tmp.close()
            self.window.iconbitmap("tmp.ico")
            Path("tmp.ico").unlink()
        except:
            pass

        self.config_file = "config.json"
        self.load_config()  # 加载配置文件

        self.create_widgets()
        self.client_socket = None
        self.running = False
        self.key = None
        self.cipher = None
        self.online_clients = {}  # 存储在线客户端信息 {id: name}
        self.selected_client = None  # 当前选中的客户端
        self.log_to_file = True  # 默认将日志存储到文件

        # 初始化日志功能
        self.init_logging()

    # 初始化日志功能    
    def init_logging(self):
        """初始化日志功能"""
        if not os.path.exists("logs"):
            os.makedirs("logs")

        # 按日期生成日志文件名
        log_filename = datetime.now().strftime("logs/client_%Y-%m-%d.log")

        # 配置日志
        logging.basicConfig(
            level=logging.INFO,
            format="%(asctime)s - %(levelname)s - %(message)s",
            handlers=[
                logging.FileHandler(log_filename,encoding='utf-8'),  # 明确指定 UTF-8 编码
                logging.StreamHandler()  # 输出到控制台
            ]
        )

    # 加载配置文件  
    def load_config(self):
        """加载配置文件"""
        if Path(self.config_file).exists():
            with open(self.config_file, 'r', encoding='utf-8') as f:
                self.config = json.load(f)
        else:
            # 默认配置
            self.config = {
                "client_name": f"客户端-{self.client_id}",
                "server_host": "127.0.0.1:12345",
                "server_list": ["127.0.0.1:12345", "nas.laokhome.cn:12345"]
            }
            self.save_config()

    # 保存配置文件  
    def save_config(self):
        """保存配置文件"""
        with open(self.config_file, 'w', encoding='utf-8') as f:
            json.dump(self.config, f, ensure_ascii=False, indent=4)

    # 创建UI组件    
    def create_widgets(self):
        # 左侧配置区域
        left_frame = ttk.Frame(self.window)
        left_frame.grid(row=0, column=0, padx=10, pady=10, sticky="nsew")

        # 客户端ID
        lbl_id = ttk.Label(left_frame, text="客户端ID:")
        lbl_id.grid(row=0, column=0, padx=5, pady=5, sticky="w")
        self.entry_id = ttk.Entry(left_frame)
        self.entry_id.insert(0, self.client_id)
        self.entry_id.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
        self.entry_id.config(state=tk.DISABLED)  # 禁用输入框

        # 客户端名称
        lbl_name = ttk.Label(left_frame, text="客户端名称:")
        lbl_name.grid(row=1, column=0, padx=5, pady=5, sticky="w")
        self.entry_name = ttk.Entry(left_frame)
        self.entry_name.insert(0, self.config["client_name"])
        self.entry_name.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
        self.entry_name.bind("<FocusOut>", self.validate_client_name)  # 绑定焦点离开事件

        # 输入框：服务器地址
        lbl_host = ttk.Label(left_frame, text="服务器地址:")
        lbl_host.grid(row=2, column=0, padx=5, pady=5, sticky="w")
        self.combo_host = ttk.Combobox(left_frame, values=self.config["server_list"])
        self.combo_host.insert(0, self.config["server_host"])
        self.combo_host.grid(row=2, column=1, padx=5, pady=5, sticky="ew")

        # # 输入框：服务器端口
        # lbl_port = ttk.Label(left_frame, text="服务器端口:")
        # lbl_port.grid(row=3, column=0, padx=5, pady=5, sticky="w")
        # self.entry_port = ttk.Entry(left_frame)
        # self.entry_port.insert(0, self.config["server_port"])
        # self.entry_port.grid(row=3, column=1, padx=5, pady=5, sticky="ew")

        # 绑定事件，当用户输入新的地址时，自动添加到下拉列表中
        def on_host_change(event):
            new_host = self.combo_host.get().strip()  # 去除前后空格
            if not new_host:  # 如果地址为空，则不处理
                return
            
            # 检查地址是否包含端口
            if ":" not in new_host:
                # 默认补全端口，假设为 HTTP 的 80 端口
                # new_host += ":80"
                # 或者根据协议自动补全，例如 HTTPS 的 443 端口
                if new_host.startswith("https://"):
                    new_host += ":443"
                else:
                    new_host += ":80"
            if new_host not in self.config["server_list"]:
                self.config["server_list"].append(new_host)
                self.combo_host["values"] = self.config["server_list"]
                self.save_config()

        self.combo_host.bind("<FocusOut>", on_host_change)

        # 连接/断开连接按钮
        self.btn_connect = ttk.Button(left_frame, text="连接到服务器", command=self.toggle_connection)
        self.btn_connect.grid(row=4, column=0, columnspan=2, padx=5, pady=5, sticky="ew")

        # 日志显示
        self.log_area = scrolledtext.ScrolledText(left_frame, height=15, width=50)
        self.log_area.grid(row=5, column=0, columnspan=2, padx=5, pady=5,sticky="nsew")

        # 消息输入框
        lbl_message = ttk.Label(left_frame, text="发送消息：")
        lbl_message.grid(row=6, column=0, padx=5, pady=5, sticky="w")
        # 使用 tk.Text 替换 ttk.Entry
        self.entry_message  = scrolledtext.ScrolledText(left_frame, height=5, width=50 ,wrap=tk.WORD)
        # self.entry_message = tk.Text(left_frame, height=5, width=50, wrap=tk.WORD)
        self.entry_message.grid(row=6, column=1, padx=5, pady=5, sticky="ew")

        # 绑定事件以限制输入长度
        def limit_input_length(event):
            current_text = self.entry_message.get("1.0", tk.END)
            if len(current_text) > 500:
                self.entry_message.delete("1.0 + 500 chars", tk.END)
                self.log("[!] 输入长度超过500字限制")

        self.entry_message.bind("<KeyRelease>", limit_input_length)

        # 发送按钮和取消选中按钮
        button_frame = ttk.Frame(left_frame)
        button_frame.grid(row=7, column=0, columnspan=2, padx=5, pady=5, sticky="ew")

        self.btn_unselect = ttk.Button(button_frame, text="取消选中", command=self.unselect_client, width=10)
        self.btn_unselect.pack(side=tk.LEFT, padx=5)
        self.btn_unselect.config(state=tk.DISABLED)  # 初始状态为禁用

        self.btn_send = ttk.Button(button_frame, text="发送", command=self.send_message, width=10)
        self.btn_send.pack(side=tk.RIGHT, padx=5)

        self.btn_close = ttk.Button(button_frame, text="关闭", command=self.close_client, width=10)
        self.btn_close.pack(side=tk.RIGHT, padx=5)

        # 右侧在线客户端列表
        right_frame = ttk.Frame(self.window)
        right_frame.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")

        lbl_clients = ttk.Label(right_frame, text="在线客户端列表:")
        lbl_clients.grid(row=0, column=0, padx=5, pady=5, sticky="w")

        self.client_listbox = tk.Listbox(right_frame, width=30, height=20)
        self.client_listbox.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
        self.client_listbox.bind("<Double-Button-1>", self.select_client)  # 双击选择客户端

        # 日志存储选项
        self.log_to_file_var = tk.BooleanVar(value=True)  # 默认勾选
        self.chk_log_to_file = ttk.Checkbutton(
            right_frame,
            text="将日志存储到文件",
            variable=self.log_to_file_var,
            command=self.toggle_log_to_file
        )
        self.chk_log_to_file.grid(row=2, column=0, padx=5, pady=5, sticky="w")

        # 布局权重配置
        self.window.grid_columnconfigure(0, weight=1)
        self.window.grid_columnconfigure(1, weight=0)
        left_frame.grid_columnconfigure(1, weight=1)
        left_frame.grid_rowconfigure(5, weight=1)

    def log(self, message):
        """在日志区域显示消息"""
        self.log_area.insert(tk.END, message + "\n")
        self.log_area.see(tk.END)
        if self.log_to_file:
            logging.info(message)  # 记录日志到文件

    def toggle_log_to_file(self):
        """切换日志存储选项"""
        self.log_to_file = self.log_to_file_var.get()

    def validate_client_name(self, event):
        """验证客户端名称是否为空"""
        if not self.entry_name.get().strip():
            messagebox.showerror("错误", "客户端名称不能为空")
            self.entry_name.insert(0, self.client_name)  # 恢复默认名称

    def toggle_connection(self):
        """连接/断开连接服务器"""
        if not self.running:
            self.connect_to_server()
        else:
            self.disconnect_from_server()

    def connect_to_server(self):
        """连接到服务器"""
        server_address = self.combo_host.get()
        try:
            host, port = server_address.split(":")
            port = int(port)
            if host == "0.0.0.0":
                host = '127.0.0.1'
        except ValueError:
            messagebox.showerror("错误", "服务器地址格式不正确，应为 'host:port'")
            return

        self.client_name = self.entry_name.get().strip()  # 获取客户端名称并去除空格
        if not self.client_name:
            messagebox.showerror("错误", "客户端名称不能为空")
            return
        try:
            self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.client_socket.connect((host, port))
            # 接收加密密钥
            self.key = self.client_socket.recv(1024)
            self.cipher = Fernet(self.key)
            # 发送客户端ID和名称
            self.client_socket.send(f"{self.client_id}:{self.client_name}".encode())
            self.log(f"[*] 成功连接到服务器 {host}:{port}")
            self.running = True
            self.btn_connect.config(text="断开连接")
            self.btn_unselect.config(state=tk.NORMAL)  # 启用取消选中按钮
            threading.Thread(target=self.receive_messages, daemon=True).start()
        except Exception as e:
            self.log(f"[!] 连接失败: {str(e)}")

    def disconnect_from_server(self):
        """断开与服务器的连接"""
        if self.client_socket:
            try:
                self.client_socket.close()
            except:
                pass
        self.running = False
        self.btn_connect.config(text="连接到服务器")
        self.btn_unselect.config(state=tk.DISABLED)  # 禁用取消选中按钮
        self.log("[*] 已断开与服务器的连接")

    def close_client(self):
        """关闭客户端"""
        # 保存当前配置
        self.config["client_name"] = self.entry_name.get()
        self.config["server_address"] = self.combo_host.get()
        self.save_config()

        self.disconnect_from_server()
        self.window.destroy()

    def send_message(self):
        """发送消息"""
        if not self.client_socket:
            messagebox.showerror("错误", "未连接到服务器")
            return
        # 发送时间
        send_time = datetime.now().strftime("%H:%M:%S")   
        # 获取多行输入框的内容
        message = self.entry_message.get("1.0", tk.END).strip()
        
        if message:
            if self.selected_client:
                # 发送私信格式: PRIVATE:target_id:message
                encrypted_message = self.cipher.encrypt(f"PRIVATE:{self.selected_client}:{message}".encode())
                self.client_socket.send(encrypted_message)
                self.log(f"[私信{send_time}] {self.online_clients[self.selected_client]}:\n {message}")
            else:
                # 发送广播消息
                encrypted_message = self.cipher.encrypt(message.encode())
                self.client_socket.send(encrypted_message)
                self.log(f"[{send_time}] 我: \n {message}")
            
            # 清空输入框
            self.entry_message.delete("1.0", tk.END)

    def receive_messages(self):
        """接收消息"""
        while self.running:
            try:
                # 接收时间
                received_time = datetime.now().strftime("%H:%M:%S")
                data = self.client_socket.recv(1024)
                if not data:
                    break
                decrypted_data = self.cipher.decrypt(data).decode()
                if decrypted_data.startswith("CLIENTS:"):  # 更新在线客户端列表
                    self.update_online_clients(decrypted_data[8:])
                elif decrypted_data.startswith("PRIVATE:"):  # 接收私信
                    _, sender_id, message = decrypted_data.split(":", 2)
                    sender_name = self.online_clients.get(sender_id, "未知客户端")
                    self.log(f"[{received_time}] {sender_name} :\n {message}")
                elif decrypted_data.startswith("BROADCAST:"):  # 接收广播消息
                    _, sender_name, message = decrypted_data.split(":", 2)
                    self.log(f"[{received_time}] {sender_name} :\n {message}")
                else:
                    # 兼容旧格式（如果没有发送者名称）
                    self.log(f"[{received_time}] 未知客户端 :\n {decrypted_data}")
            except Exception as e:
                pass
                # self.log(f"[!] 接收消息错误: {str(e)}")
                # self.log(f"[!] 已断开与服务器的连接")
                print(f"[!] 接收消息错误: {str(e)}")
                break

    def update_online_clients(self, clients_data):
        """更新在线客户端列表"""
        self.online_clients = {}
        self.client_listbox.delete(0, tk.END)
        for client_info in clients_data.split(","):
            if client_info:
                client_id, client_name = client_info.split(":")
                self.online_clients[client_id] = client_name
                self.client_listbox.insert(tk.END, f"{client_name} ({client_id})")

    def select_client(self, event):
        """双击选择客户端"""
        selection = self.client_listbox.curselection()
        if selection:
            selected_client = self.client_listbox.get(selection[0])
            client_id = selected_client.split("(")[-1].rstrip(")")
            if client_id in self.online_clients:
                self.selected_client = client_id
                self.log(f"[*] 已选择客户端: {self.online_clients[client_id]}")
                self.btn_unselect.config(state=tk.NORMAL)  # 启用取消选中按钮

    def unselect_client(self):
        """取消选中客户端"""
        self.selected_client = None
        self.log("[*] 已取消选中客户端")
        self.btn_unselect.config(state=tk.DISABLED)  # 禁用取消选中按钮

    def run(self):
        """运行客户端界面"""
        self.window.mainloop()

def main():
    client_ui = ClientUI()
    client_ui.run() 

if __name__ == "__main__":
    client_ui = ClientUI()
    client_ui.run()